//
// Authors:
//   Duncan Mak (duncan@ximian.com)
//   Ajay Kumar Dwivedi (adwiv@yahoo.com)
//   Martin Baulig (martin@gnome.org)
//
// (C) Ximian, Inc.
// Copyright (C) 2004-2006 Novell, Inc (http://www.novell.com)
// Copyright 2011 Xamarin Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
// 
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
//
// TODO:
//
//    Rewrite ToLocalTime to use GetLocalTimeDiff(DateTime,TimeSpan),
//    this should only leave the validation at the beginning (for MaxValue)
//    and then call the helper function.  This would remove all the
//    ifdefs in that code, and replace it with only one, for the construction
//    of the object.
//
//    Rewrite ToUniversalTime to use a similar setup to that
//

#ifndef TimeZone_h
#define TimeZone_h

// Riccsson includes.
#include <Typedef.h>
#include <Defines.h>
#include <System/Object.h>
#include <System/String.h>
#include <System/Collections/Generic/Dictionary.h>
#include <System/DateTime.h>
#include <System/TimeSpan.h>
#include <System/Math.h>
#include <System/Globalization/DaylightTime.h>
#include <System/Runtime/Serialization/IDeserializationCallback.h>

namespace Riccsson
{
	namespace System
	{
		class TimeZone : public object
		{
			// Properties
			PROP3GET_STATIC(TimeZone, GC_PTR<TimeZone>, CurrentTimeZone);
			PROP3GET(TimeZone, GC_PTR<string>, DaylightName);
			PROP3GET(TimeZone, GC_PTR<string>, StandardName);

			// Fields
			public: static GC_PTR<TimeZone> currentTimeZone;

			static GC_PTR<object> tz_lock;
			static long long timezone_check;

			REGISTER_ABSTRACT_CLASS(TimeZone)
				
			COPY_CONSTRUCTOR(TimeZone)
			{
			}

			// Constructor
			protected: TimeZone ();

			// Properties
			public: static PROP3_GET(GC_PTR<TimeZone>, CurrentTimeZone);

			public: abstract PROP3_GET(GC_PTR<string>, DaylightName) = 0;

			public: abstract PROP3_GET(GC_PTR<string>, StandardName) = 0;

			// Methods
			public: abstract GC_PTR<Globalization::DaylightTime> GetDaylightChanges (int year) = 0;

			public: abstract TimeSpan GetUtcOffset (DateTime time) = 0;

			public: virtual bool IsDaylightSavingTime (DateTime time);

			public: static bool IsDaylightSavingTime (DateTime time, Globalization::DaylightTime* daylightTimes);

			public: virtual DateTime ToLocalTime (DateTime time);

			public: virtual DateTime ToUniversalTime (DateTime time);

			//
			// This routine returns the TimeDiff that would have to be
			// added to "time" to turn it into a local time.   This would
			// be equivalent to call ToLocalTime.
			//
			// There is one important consideration:
			//
			//    This information is only valid during the minute it
			//    was called.
			//
			//    This only works with a real time, not one of the boundary
			//    cases like DateTime::MaxValue, so validation must be done
			//    before.
			// 
			//    This is intended to be used by DateTime::Now
			//
			// We use a minute, just to be conservative and cope with
			// any potential time zones that might be defined in the future
			// that might not nicely fit in hour or half-hour steps. 
			//    
			public: /*internal*/ TimeSpan GetLocalTimeDiff (DateTime time);

			//
			// This routine is intended to be called by GetLocalTimeDiff(DatetTime)
			// or by ToLocalTime after validation has been performed
			//
			// time is the time to map, utc_offset is the utc_offset that
			// has been computed for calling GetUtcOffset on time.
			//
			// When called by GetLocalTime, utc_offset is assumed to come
			// from a time constructed by new DateTime (DateTime::GetNow ()), that
			// is a valid time.
			//
			// When called by ToLocalTime ranges are checked before this is
			// called.
			//
			public: /*internal*/ TimeSpan GetLocalTimeDiff (DateTime time, TimeSpan utc_offset);
		};

		/*internal*/ class CurrentSystemTimeZone
			: public Runtime::Serialization::IDeserializationCallback
			, public TimeZone
		{

			// Fields
			private: GC_PTR<string> m_standardName;
			private: GC_PTR<string> m_daylightName;

			// A yearwise cache of DaylightTime.
			private: GC_PTR<Collections::Generic::Dictionary<int, GC_PTR<Globalization::DaylightTime>>> m_CachedDaylightChanges;

			// the offset when daylightsaving is not on (in ticks)
			private: long long m_ticksOffset;

			// the offset when daylightsaving is not on.
			private: TimeSpan utcOffsetWithOutDLS;
  
			// the offset when daylightsaving is on.
			private: TimeSpan utcOffsetWithDLS;

			/*internal*/ enum TimeZoneData
			{
				DaylightSavingStartIdx,
				DaylightSavingEndIdx,
				UtcOffsetIdx,
				AdditionalDaylightOffsetIdx
			};

			/*internal*/ enum TimeZoneNames
			{
				StandardNameIdx,
				DaylightNameIdx
			};

			// /*internal*/ method to get timezone data.
			//    data[0]:  start of daylight saving time (in DateTime ticks).
			//    data[1]:  end of daylight saving time (in DateTime ticks).
			//    data[2]:  utcoffset (in TimeSpan ticks).
			//    data[3]:  additional offset when daylight saving (in TimeSpan ticks).
			//    name[0]:  name of this timezone when not daylight saving.
			//    name[1]:  name of this timezone when daylight saving.
			private: static /*extern*/ bool GetTimeZoneData (int year, GC_PTR<Array<long long>> out data, GC_PTR<Array<GC_PTR<string>>> out names)
			{
#if WINDOWS
				// C# Extern method.
				
				TIME_ZONE_INFORMATION timezoneInfo;
				DWORD result = GetTimeZoneInformation(&timezoneInfo);
				switch(result)
				{
				case TIME_ZONE_ID_UNKNOWN:
					break;
				case TIME_ZONE_ID_STANDARD:
					break;
				case TIME_ZONE_ID_DAYLIGHT:
					break;
				}

				data = new Array<long long>(4);
				names = new Array<GC_PTR<string>>(2);

				// Convert wchar to char.
				char str1[32];
				char str2[32];
				for(int i = 0; str1[i] = timezoneInfo.StandardName[i]; i++)
				{
				}
				for(int i = 0; str2[i] = timezoneInfo.DaylightName[i]; i++)
				{
				}
				
				//name[0]:  name of this timezone when not daylight saving.
				//name[1]:  name of this timezone when daylight saving.
				(*names)->Insert(0, new string( str1 ) );
				(*names)->Insert(1, new string( str2 ) );
				
				//data[0]:  start of daylight saving time (in DateTime ticks).
				//data[1]:  end of daylight saving time (in DateTime ticks).
				//data[2]:  utcoffset (in TimeSpan ticks).
				//data[3]:  additional offset when daylight saving (in TimeSpan ticks).
				(*data)->Insert(0, timezoneInfo.StandardBias ); // TODO: Correct this...
				(*data)->Insert(1, timezoneInfo.StandardBias ); // TODO: Correct this...
				(*data)->Insert(2, timezoneInfo.StandardBias ); // TODO: Correct this...
				(*data)->Insert(3, timezoneInfo.StandardBias ); // TODO: Correct this...

				return true;

#else
				throw;
#endif
			}

			// Constructor
			/*internal*/ CurrentSystemTimeZone ()
			{
				m_CachedDaylightChanges = new Collections::Generic::Dictionary<int, GC_PTR<Globalization::DaylightTime>> ();
			}

			//
			// Initialized by the constructor
			//
			static int this_year;
			static GC_PTR<Globalization::DaylightTime> this_year_dlt;
		
			//
			// The "lnow" parameter must be the current time, I could have moved
			// the code here, but I do not want to interfere with serialization
			// which is why I kept the other constructor around
			//
			public: /*internal*/ CurrentSystemTimeZone (long long lnow)
			{
				m_CachedDaylightChanges = new Collections::Generic::Dictionary<int, GC_PTR<Globalization::DaylightTime>> ();

				GC_PTR<Array<long long>> data;
				GC_PTR<Array<GC_PTR<string>>> names;

				DateTime now = DateTime (lnow);
				if (!GetTimeZoneData (now.Year, data, names))
					throw NotSupportedException ("Can't get timezone name.");

				m_standardName = (**names)[(int)TimeZoneNames::StandardNameIdx];
				m_daylightName = (**names)[(int)TimeZoneNames::DaylightNameIdx];

				m_ticksOffset = (**data)[(int)TimeZoneData::UtcOffsetIdx];

				GC_PTR<Globalization::DaylightTime> dlt = GetDaylightTimeFromData (data);
				m_CachedDaylightChanges->Add (now.Year, dlt);
				OnDeserialization (dlt);
			}

			// Properties
			public: override PROP3_GET(GC_PTR<string>, DaylightName)
			{
				return m_daylightName;
			}

			public: override PROP3_GET(GC_PTR<string>, StandardName)
			{
				return m_standardName;
			}

			// Methods
			public: override GC_PTR<Globalization::DaylightTime> GetDaylightChanges (int year)
			{
				if (year < 1 || year > 9999)
					throw ArgumentOutOfRangeException ("year", year + " is not in a range between 1 and 9999.");

				//
				// First we try the case for this year, very common, and is used
				// by DateTime::Now (a popular call) indirectly.
				//
				if (year == this_year)
					return this_year_dlt;
			
				lock (m_CachedDaylightChanges)
				{
					GC_PTR<Globalization::DaylightTime> dlt;
					if (!m_CachedDaylightChanges->TryGetValue (year, dlt))
					{
						GC_PTR<Array<long long>> data;
						GC_PTR<Array<GC_PTR<string>>> names;

						if (!GetTimeZoneData (year, data, names))
							throw ArgumentException ("Can't get timezone data for " + year);

						dlt = GetDaylightTimeFromData (data);
						m_CachedDaylightChanges->Add (year, dlt);
					}

					return dlt;
				}
				
				// This will never be called.
				return null;
			}

			public: override TimeSpan GetUtcOffset (DateTime time)
			{
				if (time.Kind == DateTimeKind::Utc)
					return TimeSpan::Zero;

				if (IsDaylightSavingTime (time) && !IsAmbiguousTime (time))
					return utcOffsetWithDLS;

				return utcOffsetWithOutDLS;
			}

			private: bool IsAmbiguousTime (DateTime time)
			{
				if (time.Kind == DateTimeKind::Utc)
					return false;

				GC_PTR<Globalization::DaylightTime> changes = GetDaylightChanges (time.Year);

				return time < changes->End && time >= changes->End - changes->Delta;
			}

			void OnDeserialization (object* sender)
			{
				OnDeserialization((Globalization::DaylightTime*)null);
			}

			private: void OnDeserialization (Globalization::DaylightTime* dlt)
			{
				if (dlt == null) {
					GC_PTR<Array<long long>> data;
					GC_PTR<Array<GC_PTR<string>>> names;

					this_year = DateTime::Now.get().Year;
					if (!GetTimeZoneData (this_year, data, names))
						throw ArgumentException ("Can't get timezone data for " + this_year);
					dlt = GetDaylightTimeFromData (data);
				} else
					this_year = dlt->Start.get().Year;
			
				utcOffsetWithOutDLS = TimeSpan (m_ticksOffset);
				utcOffsetWithDLS = TimeSpan (m_ticksOffset + dlt->Delta.get().Ticks);
				this_year_dlt = dlt;
			}

			private: GC_PTR<Globalization::DaylightTime> GetDaylightTimeFromData (Array<long long>* data)
			{
				return new Globalization::DaylightTime (DateTime ((*data)[(int)TimeZoneData::DaylightSavingStartIdx]),
					DateTime ((*data)[(int)TimeZoneData::DaylightSavingEndIdx]),
					TimeSpan ((*data)[(int)TimeZoneData::AdditionalDaylightOffsetIdx]));
			}

		};
	}
}

#endif